﻿using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Threading;
using Volo.Abp;
using Volo.Abp.Caching;
using Volo.Abp.Uow;

namespace Framework.Caching.FreeRedis
{
	public class WantDistributedCache : IWantDistributedCache
	{
		public const string UowCacheName = "WantDistributedCache";

		public ILogger<WantDistributedCache> Logger { get; set; }

		protected string CacheName { get; set; }

		protected bool IgnoreMultiTenancy { get; set; }

		protected IDistributedCache Cache { get; }

		protected ICancellationTokenProvider CancellationTokenProvider { get; }

		protected IDistributedCacheSerializer Serializer { get; }

		protected IDistributedCacheKeyNormalizer KeyNormalizer { get; }

		protected IUnitOfWorkManager UnitOfWorkManager { get; }

		protected SemaphoreSlim SyncSemaphore { get; }

		protected DistributedCacheEntryOptions DefaultCacheOptions;

		private readonly AbpDistributedCacheOptions _distributedCacheOption;

		public WantDistributedCache(
			IOptions<AbpDistributedCacheOptions> distributedCacheOption,
			IDistributedCache cache,
			ICancellationTokenProvider cancellationTokenProvider,
			IDistributedCacheSerializer serializer,
			IDistributedCacheKeyNormalizer keyNormalizer,
			IUnitOfWorkManager unitOfWorkManager)
		{
			_distributedCacheOption = distributedCacheOption.Value;
			Cache = cache;
			CancellationTokenProvider = cancellationTokenProvider;
			Logger = NullLogger<WantDistributedCache>.Instance;
			Serializer = serializer;
			KeyNormalizer = keyNormalizer;
			UnitOfWorkManager = unitOfWorkManager;

		   SyncSemaphore = new SemaphoreSlim(1, 1);

			SetDefaultOptions();
		}

		protected virtual string NormalizeKey<TCacheKey>(TCacheKey key, string cacheName)
		{
			return KeyNormalizer.NormalizeKey(
				new DistributedCacheKeyNormalizeArgs(
					key.ToString(),
					cacheName,
					IgnoreMultiTenancy
				)
			);
		}

		protected virtual DistributedCacheEntryOptions GetDefaultCacheEntryOptions()
		{
			foreach (var configure in _distributedCacheOption.CacheConfigurators)
			{
				var options = configure.Invoke(CacheName);
				if (options != null)
				{
					return options;
				}
			}

			return _distributedCacheOption.GlobalCacheEntryOptions;
		}

		protected virtual void SetDefaultOptions()
		{
			//CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem));

			////IgnoreMultiTenancy
			//IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true);

			//Configure default cache entry options
			DefaultCacheOptions = GetDefaultCacheEntryOptions();
		}

		/// <summary>
		/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <returns>The cache item, or null.</returns>
		public virtual TCacheItem Get<TCacheItem, TCacheKey>(
			TCacheKey key,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			if (ShouldConsiderUow(considerUow))
			{
				var value = GetUnitOfWorkCache<TCacheItem, TCacheKey>().GetOrDefault(key)?.GetUnRemovedValueOrNull();
				if (value != null)
				{
					return value;
				}
			}

			byte[] cachedBytes;

			try
			{
				cachedBytes = Cache.Get(NormalizeKey(key, cacheName));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return null;
				}

				throw;
			}

			return ToCacheItem<TCacheItem>(cachedBytes);
		}

		public virtual KeyValuePair<TCacheKey, TCacheItem>[] GetMany<TCacheItem, TCacheKey>(
			IEnumerable<TCacheKey> keys,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			var keyArray = keys.ToArray();

			var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
			if (cacheSupportsMultipleItems == null)
			{
				return GetManyFallback<TCacheItem, TCacheKey>(
					keyArray,
					cacheName,
					hideErrors,
					considerUow
				);
			}

			var notCachedKeys = new List<TCacheKey>();
			var cachedValues = new List<KeyValuePair<TCacheKey, TCacheItem>>();
			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				foreach (var key in keyArray)
				{
					var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull();
					if (value != null)
					{
						cachedValues.Add(new KeyValuePair<TCacheKey, TCacheItem>(key, value));
					}
				}

				notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
				if (!notCachedKeys.Any())
				{
					return cachedValues.ToArray();
				}
			}

			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
			byte[][] cachedBytes;

			var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;
			try
			{
				cachedBytes = cacheSupportsMultipleItems.GetMany(readKeys.Select(key => NormalizeKey(key, cacheName)));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keyArray);
				}

				throw;
			}

			return cachedValues.Concat(ToCacheItems<TCacheItem, TCacheKey>(cachedBytes, readKeys)).ToArray();
		}

		protected virtual KeyValuePair<TCacheKey, TCacheItem>[] GetManyFallback<TCacheItem, TCacheKey>(
			TCacheKey[] keys,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				return keys
					.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(
							key,
							Get<TCacheItem, TCacheKey>(key, cacheName, false, considerUow)
						)
					).ToArray();
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keys);
				}

				throw;
			}
		}

		public virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync<TCacheItem, TCacheKey>(
			IEnumerable<TCacheKey> keys,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			var keyArray = keys.ToArray();

			var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
			if (cacheSupportsMultipleItems == null)
			{
				return await GetManyFallbackAsync<TCacheItem, TCacheKey>(
					keyArray,
					cacheName,
					hideErrors,
					considerUow,
					token
				);
			}

			var notCachedKeys = new List<TCacheKey>();
			var cachedValues = new List<KeyValuePair<TCacheKey, TCacheItem>>();
			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				foreach (var key in keyArray)
				{
					var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull();
					if (value != null)
					{
						cachedValues.Add(new KeyValuePair<TCacheKey, TCacheItem>(key, value));
					}
				}

				notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
				if (!notCachedKeys.Any())
				{
					return cachedValues.ToArray();
				}
			}

			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
			byte[][] cachedBytes;

			var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;

			try
			{
				cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(
					readKeys.Select(key => NormalizeKey(key, cacheName)),
					CancellationTokenProvider.FallbackToProvider(token)
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keyArray);
				}

				throw;
			}

			return cachedValues.Concat(ToCacheItems<TCacheItem, TCacheKey>(cachedBytes, readKeys)).ToArray();
		}

		protected virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyFallbackAsync<TCacheItem, TCacheKey>(
			TCacheKey[] keys,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();

				foreach (var key in keys)
				{
					result.Add(new KeyValuePair<TCacheKey, TCacheItem>(
						key,
						await GetAsync<TCacheItem, TCacheKey>(key, cacheName, false, considerUow, token: token))
					);
				}

				return result.ToArray();
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keys);
				}

				throw;
			}
		}

		/// <summary>
		/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
		/// <returns>The cache item, or null.</returns>
		public virtual async Task<TCacheItem> GetAsync<TCacheItem, TCacheKey>(
			TCacheKey key,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			if (ShouldConsiderUow(considerUow))
			{
				var value = GetUnitOfWorkCache<TCacheItem, TCacheKey>().GetOrDefault(key)?.GetUnRemovedValueOrNull();
				if (value != null)
				{
					return value;
				}
			}

			byte[] cachedBytes;

			try
			{
				cachedBytes = await Cache.GetAsync(
					NormalizeKey(key, cacheName),
					CancellationTokenProvider.FallbackToProvider(token)
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return null;
				}

				throw;
			}

			if (cachedBytes == null)
			{
				return null;
			}

			return Serializer.Deserialize<TCacheItem>(cachedBytes);
		}

		/// <summary>
		/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
		/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
		/// <param name="optionsFactory">The cache options for the factory delegate.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <returns>The cache item.</returns>
		public virtual TCacheItem GetOrAdd<TCacheItem, TCacheKey>(
			TCacheKey key,
			Func<TCacheItem> factory,
			string cacheName,
			Func<DistributedCacheEntryOptions> optionsFactory = null,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			var value = Get<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow);
			if (value != null)
			{
				return value;
			}

			using (SyncSemaphore.Lock())
			{
				value = Get<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow);
				if (value != null)
				{
					return value;
				}

				value = factory();

				if (ShouldConsiderUow(considerUow))
				{
					var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
					if (uowCache.TryGetValue(key, out var item))
					{
						item.SetValue(value);
					}
					else
					{
						uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
					}
				}

				Set(key, value, cacheName, optionsFactory?.Invoke(), hideErrors, considerUow);
			}

			return value;
		}

		/// <summary>
		/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
		/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
		/// <param name="optionsFactory">The cache options for the factory delegate.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
		/// <returns>The cache item.</returns>
		public virtual async Task<TCacheItem> GetOrAddAsync<TCacheItem, TCacheKey>(
			TCacheKey key,
			Func<Task<TCacheItem>> factory,
			string cacheName,
			Func<DistributedCacheEntryOptions> optionsFactory = null,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			token = CancellationTokenProvider.FallbackToProvider(token);
			var value = await GetAsync<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow, token);
			if (value != null)
			{
				return value;
			}

			using (await SyncSemaphore.LockAsync(token))
			{
				value = await GetAsync<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow, token);
				if (value != null)
				{
					return value;
				}

				value = await factory();

				if (ShouldConsiderUow(considerUow))
				{
					var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
					if (uowCache.TryGetValue(key, out var item))
					{
						item.SetValue(value);
					}
					else
					{
						uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
					}
				}

				await SetAsync(key, value, cacheName, optionsFactory?.Invoke(), hideErrors, considerUow, token);
			}

			return value;
		}

		/// <summary>
		/// Sets the cache item value for the provided key.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="value">The cache item value to set in the cache.</param>
		/// <param name="options">The cache options for the value.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		public virtual void Set<TCacheItem, TCacheKey>(
			TCacheKey key,
			TCacheItem value,
			string cacheName,
			DistributedCacheEntryOptions options = null,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			void SetRealCache()
			{
				hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

				try
				{
					Cache.Set(
						NormalizeKey(key, cacheName),
						Serializer.Serialize(value),
						options ?? DefaultCacheOptions
					);
				}
				catch (Exception ex)
				{
					if (hideErrors == true)
					{
						HandleException(ex);
						return;
					}

					throw;
				}
			}

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out _))
				{
					uowCache[key].SetValue(value);
				}
				else
				{
					uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
				}

				// ReSharper disable once PossibleNullReferenceException
				UnitOfWorkManager.Current.OnCompleted(() =>
				{
					SetRealCache();
					return Task.CompletedTask;
				});
			}
			else
			{
				SetRealCache();
			}
		}
		/// <summary>
		/// Sets the cache item value for the provided key.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="value">The cache item value to set in the cache.</param>
		/// <param name="options">The cache options for the value.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
		public virtual async Task SetAsync<TCacheItem, TCacheKey>(
			TCacheKey key,
			TCacheItem value,
			string cacheName,
			DistributedCacheEntryOptions options = null,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			async Task SetRealCache()
			{
				hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

				try
				{
					await Cache.SetAsync(
						NormalizeKey(key, cacheName),
						Serializer.Serialize(value),
						options ?? DefaultCacheOptions,
						CancellationTokenProvider.FallbackToProvider(token)
					);
				}
				catch (Exception ex)
				{
					if (hideErrors == true)
					{
						await HandleExceptionAsync(ex);
						return;
					}

					throw;
				}
			}

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out _))
				{
					uowCache[key].SetValue(value);
				}
				else
				{
					uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
				}

				// ReSharper disable once PossibleNullReferenceException
				UnitOfWorkManager.Current.OnCompleted(SetRealCache);
			}
			else
			{
				await SetRealCache();
			}
		}

		public void SetMany<TCacheItem, TCacheKey>(
			IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
			string cacheName,
			DistributedCacheEntryOptions options = null,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			var itemsArray = items.ToArray();

			var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
			if (cacheSupportsMultipleItems == null)
			{
				SetManyFallback(
					itemsArray,
					cacheName,
					options,
					hideErrors,
					considerUow
				);

				return;
			}

			void SetRealCache()
			{
				hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

				try
				{
					cacheSupportsMultipleItems.SetMany(
						ToRawCacheItems(itemsArray, cacheName),
						options ?? DefaultCacheOptions
					);
				}
				catch (Exception ex)
				{
					if (hideErrors == true)
					{
						HandleException(ex);
						return;
					}

					throw;
				}
			}

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();

				foreach (var pair in itemsArray)
				{
					if (uowCache.TryGetValue(pair.Key, out _))
					{
						uowCache[pair.Key].SetValue(pair.Value);
					}
					else
					{
						uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
					}
				}

				// ReSharper disable once PossibleNullReferenceException
				UnitOfWorkManager.Current.OnCompleted(() =>
				{
					SetRealCache();
					return Task.CompletedTask;
				});
			}
			else
			{
				SetRealCache();
			}
		}

		protected virtual void SetManyFallback<TCacheItem, TCacheKey>(
			KeyValuePair<TCacheKey, TCacheItem>[] items,
			string cacheName,
			DistributedCacheEntryOptions options = null,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				foreach (var item in items)
				{
					Set(
						item.Key,
						item.Value,
						cacheName,
						options,
						false,
						considerUow
					);
				}
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		public virtual async Task SetManyAsync<TCacheItem, TCacheKey>(
			IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
			string cacheName,
			DistributedCacheEntryOptions options = null,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			var itemsArray = items.ToArray();

			var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
			if (cacheSupportsMultipleItems == null)
			{
				await SetManyFallbackAsync(
					itemsArray,
					cacheName,
					options,
					hideErrors,
					considerUow,
					token
				);

				return;
			}

			async Task SetRealCache()
			{
				hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

				try
				{
					await cacheSupportsMultipleItems.SetManyAsync(
						ToRawCacheItems(itemsArray, cacheName),
						options ?? DefaultCacheOptions,
						CancellationTokenProvider.FallbackToProvider(token)
					);
				}
				catch (Exception ex)
				{
					if (hideErrors == true)
					{
						await HandleExceptionAsync(ex);
						return;
					}

					throw;
				}
			}

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();

				foreach (var pair in itemsArray)
				{
					if (uowCache.TryGetValue(pair.Key, out _))
					{
						uowCache[pair.Key].SetValue(pair.Value);
					}
					else
					{
						uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
					}
				}

				// ReSharper disable once PossibleNullReferenceException
				UnitOfWorkManager.Current.OnCompleted(SetRealCache);
			}
			else
			{
				await SetRealCache();
			}
		}

		protected virtual async Task SetManyFallbackAsync<TCacheItem, TCacheKey>(
			KeyValuePair<TCacheKey, TCacheItem>[] items,
			string cacheName,
			DistributedCacheEntryOptions options = null,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				foreach (var item in items)
				{
					await SetAsync(
						item.Key,
						item.Value,
						cacheName,
						options,
						false,
						considerUow,
						token: token
					);
				}
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		/// <summary>
		/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		public virtual void Refresh<TCacheKey>(
			TCacheKey key,
			string cacheName,
			bool? hideErrors = null)
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				Cache.Refresh(NormalizeKey(key, cacheName));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		/// <summary>
		/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
		public virtual async Task RefreshAsync<TCacheKey>(
			TCacheKey key,
			string cacheName,
			bool? hideErrors = null,
			CancellationToken token = default)
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await Cache.RefreshAsync(NormalizeKey(key, cacheName), CancellationTokenProvider.FallbackToProvider(token));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		/// <summary>
		/// Removes the cache item for given key from cache.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		public virtual void Remove<TCacheItem, TCacheKey>(
			TCacheKey key,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false) where TCacheItem : class
		{
			void RemoveRealCache()
			{
				hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

				try
				{
					Cache.Remove(NormalizeKey(key, cacheName));
				}
				catch (Exception ex)
				{
					if (hideErrors == true)
					{
						HandleException(ex);
						return;
					}

					throw;
				}
			}

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out _))
				{
					uowCache[key].RemoveValue();
				}

				// ReSharper disable once PossibleNullReferenceException
				UnitOfWorkManager.Current.OnCompleted(() =>
				{
					RemoveRealCache();
					return Task.CompletedTask;
				});
			}
			else
			{
				RemoveRealCache();
			}
		}

		/// <summary>
		/// Removes the cache item for given key from cache.
		/// </summary>
		/// <param name="key">The key of cached item to be retrieved from the cache.</param>
		/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
		/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
		/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
		public virtual async Task RemoveAsync<TCacheItem, TCacheKey>(
			TCacheKey key,
			string cacheName,
			bool? hideErrors = null,
			bool considerUow = false,
			CancellationToken token = default) where TCacheItem : class
		{
			async Task RemoveRealCache()
			{
				hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

				try
				{
					await Cache.RemoveAsync(NormalizeKey(key, cacheName), CancellationTokenProvider.FallbackToProvider(token));
				}
				catch (Exception ex)
				{
					if (hideErrors == true)
					{
						await HandleExceptionAsync(ex);
						return;
					}

					throw;
				}
			}

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out _))
				{
					uowCache[key].RemoveValue();
				}

				// ReSharper disable once PossibleNullReferenceException
				UnitOfWorkManager.Current.OnCompleted(RemoveRealCache);
			}
			else
			{
				await RemoveRealCache();
			}
		}

		protected virtual void HandleException(Exception ex)
		{
			AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
		}

		protected virtual async Task HandleExceptionAsync(Exception ex)
		{
			Logger.LogException(ex, LogLevel.Warning);

			//using (var scope = ServiceScopeFactory.CreateScope())
			//{
			//	await scope.ServiceProvider
			//		.GetRequiredService<IExceptionNotifier>()
			//		.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
			//}
		}

		protected virtual KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItems<TCacheItem, TCacheKey>(byte[][] itemBytes, TCacheKey[] itemKeys) where TCacheItem : class
		{
			if (itemBytes.Length != itemKeys.Length)
			{
				throw new AbpException("count of the item bytes should be same with the count of the given keys");
			}

			var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();

			for (int i = 0; i < itemKeys.Length; i++)
			{
				result.Add(
					new KeyValuePair<TCacheKey, TCacheItem>(
						itemKeys[i],
						ToCacheItem<TCacheItem>(itemBytes[i])
					)
				);
			}

			return result.ToArray();
		}

		[CanBeNull]
		protected virtual TCacheItem ToCacheItem<TCacheItem>([CanBeNull] byte[] bytes) where TCacheItem : class
		{
			if (bytes == null)
			{
				return null;
			}

			return Serializer.Deserialize<TCacheItem>(bytes);
		}


		protected virtual KeyValuePair<string, byte[]>[] ToRawCacheItems<TCacheItem, TCacheKey>(KeyValuePair<TCacheKey, TCacheItem>[] items, string cacheName)
		{
			return items
				.Select(i => new KeyValuePair<string, byte[]>(
						NormalizeKey(i.Key, cacheName),
						Serializer.Serialize(i.Value)
					)
				).ToArray();
		}

		private static KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(TCacheKey[] keys)
		{
			return keys
				.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(key, default))
				.ToArray();
		}

		protected virtual bool ShouldConsiderUow(bool considerUow)
		{
			return considerUow && UnitOfWorkManager.Current != null;
		}

		protected virtual string GetUnitOfWorkCacheKey()
		{
			return UowCacheName + CacheName;
		}

		protected virtual Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>> GetUnitOfWorkCache<TCacheItem, TCacheKey>() where TCacheItem : class
		{
			if (UnitOfWorkManager.Current == null)
			{
				throw new AbpException($"There is no active UOW.");
			}

			return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(),
				key => new Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>>());
		}
	}
}
